package nl.zoidberg.calculon.icc;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;

import nl.zoidberg.calculon.engine.BitBoard;
import nl.zoidberg.calculon.engine.Board;
import nl.zoidberg.calculon.engine.ChessEngine;
import nl.zoidberg.calculon.model.Piece;
import nl.zoidberg.calculon.notation.PGNUtils;
import nl.zoidberg.calculon.notation.Style12;
import nl.zoidberg.calculon.util.LogFormatter;

import org.apache.commons.digester.Digester;
import org.apache.commons.lang.StringUtils;

public class ICCInterface {

	private static final Logger log = Logger.getLogger(ICCInterface.class.getName());

	private static final int DG_MY_GAME_STARTED 		= 15;
	private static final int DG_MY_GAME_RESULT 			= 16;

	private static boolean shutdown = false;
	private static String talkResponse = "I'm sorry Dave, I'm afraid I can't do that.";
	private static ICCSConfig iccConfig;

	private Socket connection;
	private Thread moveThread = null;
	private List<ConnectionListener> listeners = new ArrayList<ConnectionListener>();
	private List<BlockHandler> blockHandlers = new ArrayList<BlockHandler>();
	private PrintStream out;
	private String opponent = null;
	private boolean rated = false;
	private int gameNumber = -1;
	private boolean playingWhite = true;
	private boolean accept = true;
	private boolean alive = true;
	private BitBoard currentBoard;
	
	public static void main(String[] args) throws Exception {
		log.setLevel(Level.FINE);
		Handler handler = new ConsoleHandler();
		handler.setLevel(Level.FINE);
		handler.setFormatter(new LogFormatter());
		
		for(Handler h: log.getHandlers()) {
			log.removeHandler(h);
		}
		log.addHandler(handler);
		
		if(System.getProperty("calculon.password") == null)
		{
			log.severe("password must be specified.");
			System.exit(-1);
		}
		
		while(!shutdown) {
			try {
				new ICCInterface().connect();
			} catch (Exception x) {
				log.log(Level.SEVERE, "Error", x);
				try { Thread.sleep(60000); } catch (InterruptedException ix) { }
			}
		}
	}

	private ICCInterface() {
		Digester digester = new Digester();
		
		digester.addObjectCreate("calculon/icc", ICCSConfig.class);
		digester.addBeanPropertySetter("calculon/icc/operator-name", "operatorName");
		digester.addBeanPropertySetter("calculon/icc/login-name", "loginName");
		digester.addBeanPropertySetter("calculon/icc/accept-min", "acceptMin");
		digester.addBeanPropertySetter("calculon/icc/accept-max", "acceptMax");
		digester.addBeanPropertySetter("calculon/icc/max-rematches", "maxRematches");
		digester.addBeanPropertySetter("calculon/icc/reseek", "reseek");
		digester.addObjectCreate("calculon/icc/default-seeks/seek", ICCSConfig.Seek.class);
		digester.addSetProperties("calculon/icc/default-seeks/seek", "time", "initialTime");
		digester.addSetProperties("calculon/icc/default-seeks/seek", "inc", "increment");
		digester.addSetNext("calculon/icc/default-seeks/seek", "addSeekAd");
		
		try {
			iccConfig = (ICCSConfig) digester.parse(ClassLoader.getSystemResourceAsStream("calculon.xml"));
		} catch (Exception e) {
			log.log(Level.WARNING, "Config reading failed", e);
			throw new RuntimeException(e);
		}
		log.finer(iccConfig.toString());
		
		listeners.add(new DebugListener());
		listeners.add(new ChallengeListener());
		listeners.add(new BoardListener());
		listeners.add(new AbortListener());
		listeners.add(new CommandListener());
		listeners.add(new ReseekListener());
		listeners.add(new ChatListener());
		
		listeners.add(new BlockListener());
		listeners.add(new BlockLv2Listener());
		
		blockHandlers.add(new GameEndedHandler());
	}

	public void connect() throws IOException {
		connection = new Socket("chessclub.com", 23);
		doLogin();
		BufferedReader reader = new BufferedReader(new InputStreamReader(
				connection.getInputStream()));
		out = new PrintStream(connection.getOutputStream());

		send("set level1 1");
		send("set style 12");
		receiveLevel2(DG_MY_GAME_STARTED);
		receiveLevel2(DG_MY_GAME_RESULT);
		
		setStatus();
		if (iccConfig.isReseek()) {
			reseek();
		}

		Runnable keepAlive = new Runnable() {
			public void run() {
				while(alive) {
					send("date");
					try { Thread.sleep(60000 * 15); } catch (InterruptedException x) { }
				}
			}
		};
		Thread keepAliveThread = new Thread(keepAlive);
		keepAliveThread.setDaemon(true);
		keepAliveThread.start();

		StringBuffer line = new StringBuffer();
		int c;
		try {
			while((c = reader.read()) != -1) {
				if(c == ('M'&0x1F) || c == ('G'&0x1F)) {
					continue;
				}
				line.append((char)c);
				if(c == '\n') {
					fireDataReceived(line.toString());
					line.setLength(0);
					continue;
				}
				if(line.length() >= 2 && line.charAt(line.length()-2) == ('Y'&0x1F) && line.charAt(line.length()-1) == ']') {
					fireDataReceived(line.toString());
					line.setLength(0);
				}
			}
		} finally {
			alive = false;
			try {
				reader.close();
				out.close();
			} catch (Exception x) { }
		}
	}
	
	private void receiveLevel2(int dgId) {
		send("set-2 " + String.valueOf(dgId) + " 1"); 
	}

	private void fireDataReceived(String s) {
		for (ConnectionListener listener : listeners) {
			try {
				listener.message(s);
			} catch (Exception e) {
				log.log(Level.WARNING, "Handler " + listener + " threw exception", e);
			}
		}
	}

	private void doLogin() throws IOException {
		int c;
		String sLogin = "login: ";
		int sptr = 0;
		while ((c = connection.getInputStream().read()) != -1) {
			if (c == sLogin.charAt(sptr)) {
				sptr++;
				if (sptr == sLogin.length()) {
					log.fine("Sending login name");
					connection.getOutputStream()
							.write((iccConfig.getLoginName() + "\n").getBytes());
					break;
				}
			} else {
				sptr = 0;
			}
		}

		sLogin = "password: ";
		sptr = 0;
		while ((c = connection.getInputStream().read()) != -1) {
			if (c == sLogin.charAt(sptr)) {
				sptr++;
				if (sptr == sLogin.length()) {
					log.finer("Sending password (" + System.getProperty("calculon.password") + ")");
					connection.getOutputStream().write((System.getProperty("calculon.password") + "\n").getBytes());
					break;
				}
			} else {
				sptr = 0;
			}
		}
	}
	
	private void reseek() {
		send("resume");
		Runnable seeker = new Runnable() {
			public void run() {
				for(int i = 0; i < 4; i++) {
					try { Thread.sleep(15000); } catch (InterruptedException x) { }
					if(gameNumber != -1) {
							return;
					}
					send("resume");
				}
				for(ICCSConfig.Seek seek: iccConfig.getSeekAds()) {
					send(seek.getCommand());
				}
			}
		};
		new Thread(seeker).start();
	}

	private synchronized void send(String s) {
		log.fine(">>> " + s);
		out.println(s);
	}

	private void tellOp(String s) {
		send("tell " + iccConfig.getOperatorName() + " " + s);
	}
	
	private void setStatus() {
		if(shutdown) {
			send("set 9 Current Status: Shutting down.");
		} else if (iccConfig.isReseek()) {
			send("set 9 Current Status: Auto (accept " + (accept?"on":"off") + ").");
		} else {
			send("set 9 Current Status: Manual (accept " + (accept?"on":"off") + ").");
		}
	}

	private interface ConnectionListener {
		public void message(String s);
	}
	
	private interface BlockHandler {
		@Deprecated
		public void processBlock(ResponseBlock responseBlock);
		public void processBlock(ResponseBlockLv2 responseBlock);
	}

	private class DebugListener implements ConnectionListener {
		public void message(String s) {
			log.finest("<<< " + s);
		}
	}

	private class ReseekListener implements ConnectionListener {
		public void message(String s) {
		}
	}

	private class ChallengeListener implements ConnectionListener {
		// e.g. Challenge: BarryNL (2029) CalculonX (2000) rated Blitz 5 0
		
		public void message(String s) {
			if (s.startsWith("Challenge: ") && !accept) {
				send("decline");
				return;
			}
			
			if (s.startsWith("Challenge: ") && accept) {
				String[] args = StringUtils.split(s);
				int gameLength = Integer.parseInt(args[args.length-2])*60 + Integer.parseInt(args[args.length-1])*40;
				
				if("rated".equals(args[args.length-4]) 
						&& gameLength >= iccConfig.getAcceptMin() && gameLength <= iccConfig.getAcceptMax()) {
					log.fine("Accepting: '" + s + "' " + gameLength + "s");
					send("accept");
				} else {
					log.fine("Rejecting: '" + s + "' " + gameLength + "s");
					send("decline");
				}
				return;
			}
			if (s.startsWith("Creating: ")) {
				// TODO: This doesn't work for ICC
				log.info("Starting game: '" + s + "'");
				List<String> fields = Arrays.asList(StringUtils.split(s));
				playingWhite = iccConfig.getLoginName().equals(fields.get(1));
				opponent = playingWhite ? fields.get(3) : fields.get(1);
				rated = "rated".equals(fields.get(5));
				send("finger " + opponent);
			}
		}
	}

	private class AbortListener implements ConnectionListener {
		public void message(String s) {
			if (opponent != null
					&& s.startsWith(opponent + " would like to abort the game;")
					&& !rated) {
				send("abort");
			}
		}
	}

	private class ChatListener implements ConnectionListener {
		public void message(String s) {
			
			if(s.startsWith(iccConfig.getOperatorName() + " ")) {
				return;
			}
			
			String[] fields = StringUtils.split(s);
			if(fields.length >= 3 && "tells".equals(fields[1]) && "you:".equals(fields[2])) {
				send("tell " + fields[0] + " " + talkResponse);
			}
			if(fields.length >= 3 && "says:".equals(fields[1])) {
				send("say " + talkResponse);
			}
		}
	}

	private class CommandListener implements ConnectionListener {
		public void message(String s) {
			if (!s.startsWith(iccConfig.getOperatorName() + " tells you: ")) {
				return;
			}
			
			List<String> words = Arrays.asList(StringUtils.split(s));
			if (words.size() < 4) {
				return;
			}

			if ("do".equals(words.get(3))) {
				StringBuffer buf = new StringBuffer();
				for (int i = 4; i < words.size(); i++) {
					buf.append(words.get(i)).append(" ");
				}
				send(buf.toString().trim());
				tellOp("sent '" + buf.toString().trim() + "'.");
			}

			if ("shutdown".equals(words.get(3))) {
				tellOp("Will shutdown after current game.");
				shutdown = true;
				iccConfig.setReseek(false);
				accept = false;
				setStatus();
			}
			
			if ("accept".equals(words.get(3))) {
				if(words.size() > 4 && "on".equals(words.get(4))) {
					accept = true;
					shutdown = false;
				} else {
					accept = false;
				}
				tellOp("accept " + (accept ? "on" : "off"));
				setStatus();
			}
			
			if (words.size() > 4 && "reseek".equals(words.get(3))) {
				if("on".equals(words.get(4))) {
					iccConfig.setReseek(true);
					shutdown = false;
				} else {
					iccConfig.setReseek(false);
				}
				tellOp("reseek " + (iccConfig.isReseek() ? "on" : "off"));
				setStatus();
			}
		}
	}
	
	private class GameEndedHandler implements BlockHandler {
		public void processBlock(ResponseBlock responseBlock) {
			// Deprecated
		}

		public void processBlock(ResponseBlockLv2 responseBlock) {
			if(responseBlock.getCode() != DG_MY_GAME_RESULT) {
				return;
			}

			log.info("Game ends: [" + responseBlock.getData() + "]");
			currentBoard = null;
			gameNumber = -1;
			opponent = null;

			while(moveThread != null && moveThread.isAlive()) {
				try { Thread.sleep(200); } catch (InterruptedException x) { }
			}
			
			if (shutdown) {
				send("quit");
			} else if (iccConfig.isReseek()) {
				reseek();
			}
		}
	}
	
	private class BoardListener implements ConnectionListener {
		public void message(String s) {
			if (!s.startsWith("<12> ")) {
				return;
			}
			
			final Style12 style12 = new Style12(s);
			
			if(style12.isMyGame()) {
				gameNumber = style12.getGameNumber();
				opponent = style12.getOpponentName();
				playingWhite = (style12.getMyColor() == Piece.WHITE);
				if(style12.isInitialPosition()) {
					currentBoard = new BitBoard(new Board().initialise());
				}
			}
			
			if ( ! (style12.getMyRelationToGame() == Style12.REL_ME_TO_MOVE)) {
				return;
			}
			
			if(style12.isFlagged()) {
				gameNumber = -1;
				currentBoard = null;
				return;
			}

			if(style12.getHalfMoveCount() >= 100) {
				log.info("Claiming draw by 50-move rule");
				send("draw");
				return;
			}
			
			if(currentBoard != null && !"none".equals(style12.getPreviousMovePGN())) {
				try {
					currentBoard.makeMove(currentBoard.getMove(PGNUtils.toPgnMoveMap(currentBoard).get(style12.getPreviousMovePGN())));
				} catch (Exception x) {
					log.log(Level.SEVERE, "Apply move failed", x);
				}
			}
			
			if(currentBoard == null || ! currentBoard.equalPosition(new BitBoard(style12.getBoard()))) {
				log.warning("Out of sync board detected - resetting!");
				currentBoard = new BitBoard(style12.getBoard());
			}
			
			if(currentBoard.getRepeatedCount() >= 3) {
				log.info("Claiming draw by 3-fold repitition (opp move)");
				send("draw");
				return;
			}
			
			Runnable moveMaker = new Runnable() {
				public void run() {
					BitBoard myBoard = currentBoard;
					ChessEngine engine = new ChessEngine();
					String bestMove = engine.getPreferredMove(myBoard);
					if(bestMove != null) {
						if(gameNumber != -1) {
							log.info("Moving: " + PGNUtils.translateMove(new Board(myBoard), bestMove));
							if(currentBoard != null) {
								currentBoard.makeMove(currentBoard.getMove(bestMove));
//								currentBoard.applyMove(bestMove);
							}
							send(bestMove.toLowerCase());
							if(currentBoard.getRepeatedCount() >= 3) {
								log.info("Claiming draw by 3-fold repitition (my move)");
								send("draw");
							}
						} else {
							log.info("Game not active - move aborted");
						}
					}
					moveThread = null;
				}
			};
			
			moveThread = new Thread(moveMaker);
			moveThread.start();
		}
	}
	
	@Deprecated
	private class BlockListener implements ConnectionListener {
		private StringBuffer currentBlock = new StringBuffer();
		private int blockLevel = 0;
		
		public void message(String s) {
			for(int i = 0; i < s.length(); i++) {
				if(s.charAt(i) == ('Y'&0x1F) && s.charAt(i+1) == '[') {
					if(blockLevel == 0) {
						currentBlock.setLength(0);
					}
					blockLevel++;
				}
				if(blockLevel > 0) {
					currentBlock.append(s.charAt(i));
				}
				if(blockLevel > 0 && s.charAt(i) == ']' && s.charAt(i-1) == ('Y'&0x1F)) {
					blockLevel--;
					if(blockLevel == 0) {
						for(ResponseBlock block: parseBlockResponse(currentBlock.toString())) {
							for(BlockHandler handler: blockHandlers) {
								handler.processBlock(block);
							}
						}
					}
				}
			}
			if(blockLevel > 0) {
				currentBlock.append("\n");
			}
		}
	}
	
	private class BlockLv2Listener implements ConnectionListener {
		private StringBuffer currentBlock = new StringBuffer();
		
		public void message(String s) {
			for(int i = 0; i < s.length(); i++) {
				if(s.charAt(i) == ('Y'&0x1F) && s.charAt(i+1) == '(') {
					currentBlock.setLength(0);
				}
				currentBlock.append(s.charAt(i));
				if(s.charAt(i) == ')' && s.charAt(i-1) == ('Y'&0x1F)) {
					for(ResponseBlockLv2 block: parseBlockResponseLv2(currentBlock.toString())) {
						log.fine("LV2: " + block);
						for(BlockHandler handler: blockHandlers) {
							handler.processBlock(block);
						}
					}
				}
			}
			currentBlock.append("\n");
		}
	}
	
	@Deprecated
	private List<ResponseBlock> parseBlockResponse(String s) {
		List<ResponseBlock> rv = new ArrayList<ResponseBlock>();

		StringBuffer buf = new StringBuffer(s);
		
		Stack<StringBuffer> allBlocks = new Stack<StringBuffer>();
		for(int i = 0; i < buf.length(); i++) {
			if(buf.charAt(i) == ('Y'&0x1F) && buf.charAt(i+1) == '[') {
				allBlocks.push(new StringBuffer());
				i++;
				continue;
			}
			if(buf.charAt(i) == ('Y'&0x1F) && buf.charAt(i+1) == ']') {
				rv.add(new ResponseBlock(allBlocks.pop().toString()));
				i++;
				continue;
			}
			allBlocks.peek().append(buf.charAt(i));
		}
		
		log.fine(String.valueOf(rv));
		return rv;
	}

	private List<ResponseBlockLv2> parseBlockResponseLv2(String s) {
		List<ResponseBlockLv2> rv = new ArrayList<ResponseBlockLv2>();

		StringBuffer buf = new StringBuffer(s);
		
		Stack<StringBuffer> allBlocks = new Stack<StringBuffer>();
		for(int i = 0; i < buf.length(); i++) {
			if(buf.charAt(i) == ('Y'&0x1F) && buf.charAt(i+1) == '(') {
				allBlocks.push(new StringBuffer());
				i++;
				continue;
			}
			if(buf.charAt(i) == ('Y'&0x1F) && buf.charAt(i+1) == ')') {
				rv.add(new ResponseBlockLv2(allBlocks.pop().toString()));
				i++;
				continue;
			}
			allBlocks.peek().append(buf.charAt(i));
		}
		
		log.fine(String.valueOf(rv));
		return rv;
	}

	@Deprecated
	private class ResponseBlock {
		private String data;
		private int code;
		private String user;
		private List<String> lines = new ArrayList<String>();
		
		private ResponseBlock(String s) {
			this.data = s;
			StringTokenizer st = new StringTokenizer(s);
			code = Integer.parseInt(st.nextToken());
			user = st.nextToken();
			
			for(st = new StringTokenizer(s, "\n"); st.hasMoreTokens(); ) {
				lines.add(st.nextToken());
			}
			lines.remove(0); // First line is the code/user
		}
		
		public List<String> getLines() {
			return lines;
		}

		public int getCode() {
			return code;
		}

		public void setCode(int code) {
			this.code = code;
		}

		public String getUser() {
			return user;
		}

		public void setUser(String user) {
			this.user = user;
		}

		public String getData() {
			return data;
		}
		public void setData(String data) {
			this.data = data;
		}

		/**
		 * Constructs a <code>String</code> with all attributes
		 * in name = value format.
		 *
		 * @return a <code>String</code> representation 
		 * of this object.
		 */
		public String toString()
		{
			StringBuffer buf = new StringBuffer("");
			buf.append("ResponseBlock(code=").append(code);
			buf.append(",user=").append(user).append(")\n=============\n");
			for(String s: lines) {
				buf.append(s).append("\n");
			}
			buf.append("=============");
			return buf.toString();
		}
	}
	
	private class ResponseBlockLv2 {
		private String data;
		private int code;
		
		private ResponseBlockLv2(String s) {
			this.data = s;
			StringTokenizer st = new StringTokenizer(s);
			code = Integer.parseInt(st.nextToken());
		}
		
		public int getCode() {
			return code;
		}

		public void setCode(int code) {
			this.code = code;
		}

		public String getData() {
			return data;
		}
		public void setData(String data) {
			this.data = data;
		}

		/**
		 * Constructs a <code>String</code> with all attributes
		 * in name = value format.
		 *
		 * @return a <code>String</code> representation 
		 * of this object.
		 */
		public String toString()
		{
			StringBuffer buf = new StringBuffer("");
			buf.append("ResponseBlock(code=").append(code).append(" = ").append(data);
			return buf.toString();
		}
	}
}
